{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "881ebde7-66bf-443f-815c-dc0b174d58a9",
   "metadata": {
    "ExecutionIndicator": {
     "show": true
    },
    "execution": {
     "iopub.execute_input": "2025-09-21T12:44:07.683155Z",
     "iopub.status.busy": "2025-09-21T12:44:07.682909Z",
     "iopub.status.idle": "2025-09-21T13:55:44.087169Z",
     "shell.execute_reply": "2025-09-21T13:55:44.086640Z",
     "shell.execute_reply.started": "2025-09-21T12:44:07.683133Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/usr/local/lib/python3.11/site-packages/torchvision/models/_utils.py:208: UserWarning: The parameter 'pretrained' is deprecated since 0.13 and may be removed in the future, please use 'weights' instead.\n",
      "  warnings.warn(\n",
      "/usr/local/lib/python3.11/site-packages/torchvision/models/_utils.py:223: UserWarning: Arguments other than a weight enum or `None` for 'weights' are deprecated since 0.13 and may be removed in the future. The current behavior is equivalent to passing `weights=MobileNet_V2_Weights.IMAGENET1K_V1`. You can also use `weights=MobileNet_V2_Weights.DEFAULT` to get the most up-to-date weights.\n",
      "  warnings.warn(msg)\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch 01 | Train loss 0.9506 acc 0.7464 | Val loss 0.4297 acc 0.8975\n",
      "Epoch 02 | Train loss 0.4379 acc 0.8742 | Val loss 0.2916 acc 0.9339\n",
      "Epoch 03 | Train loss 0.3505 acc 0.8977 | Val loss 0.2392 acc 0.9457\n",
      "Epoch 04 | Train loss 0.3135 acc 0.9055 | Val loss 0.1967 acc 0.9552\n",
      "Epoch 05 | Train loss 0.2785 acc 0.9149 | Val loss 0.1783 acc 0.9563\n",
      "Epoch 06 | Train loss 0.2628 acc 0.9193 | Val loss 0.1667 acc 0.9563\n",
      "Epoch 07 | Train loss 0.2493 acc 0.9211 | Val loss 0.1343 acc 0.9636\n",
      "Epoch 08 | Train loss 0.2445 acc 0.9227 | Val loss 0.1408 acc 0.9617\n",
      "Epoch 09 | Train loss 0.2311 acc 0.9256 | Val loss 0.1383 acc 0.9617\n",
      "Epoch 10 | Train loss 0.2316 acc 0.9253 | Val loss 0.1332 acc 0.9658\n",
      "Test accuracy = 0.8662\n"
     ]
    }
   ],
   "source": [
    "import torch, random, numpy as np, pandas as pd\n",
    "from sklearn.model_selection import train_test_split\n",
    "from torchvision import transforms, models\n",
    "from torch.utils.data import Dataset, DataLoader\n",
    "import torch.nn as nn\n",
    "\n",
    "# 0. 可复现\n",
    "torch.manual_seed(42)\n",
    "np.random.seed(42)\n",
    "random.seed(42)\n",
    "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n",
    "\n",
    "# 1. 读 csv --------------------------------------------------------------------\n",
    "train_df = pd.read_csv('data/shouyuminst/sign_mnist_train.csv')\n",
    "test_df  = pd.read_csv('data/shouyuminst/sign_mnist_test.csv')\n",
    "\n",
    "\n",
    "train_df = train_df[train_df['label'] != 24]\n",
    "test_df  = test_df[test_df['label']  != 24]\n",
    "\n",
    "x_train = train_df.drop(columns='label').values.astype(np.float32)\n",
    "y_train = train_df['label'].values.astype(np.int64)\n",
    "x_test  = test_df.drop(columns='label').values.astype(np.float32)\n",
    "y_test  = test_df['label'].values.astype(np.int64)\n",
    "\n",
    "\n",
    "# 2. 28×28 灰度 → 96×96 RGB ----------------------------------------------------\n",
    "class SignMNISTDataset(Dataset):\n",
    "    def __init__(self, x, y, transform=None):\n",
    "        self.x = x\n",
    "        self.y = y\n",
    "        self.transform = transform\n",
    "\n",
    "    def __len__(self):\n",
    "        return len(self.y)\n",
    "\n",
    "    def __getitem__(self, idx):\n",
    "        img = self.x[idx].reshape(28, 28)\n",
    "        # 重复 3 次变成 3 通道\n",
    "        img = np.stack([img, img, img], axis=-1)\n",
    "        if self.transform:\n",
    "            img = self.transform(img)\n",
    "        return img, self.y[idx]\n",
    "\n",
    "# 3. 数据增强 / 归一化（ImageNet 统计值）\n",
    "train_tf = transforms.Compose([\n",
    "    transforms.ToPILImage(),\n",
    "    transforms.Resize((96, 96)),\n",
    "    transforms.RandomHorizontalFlip(0.2),\n",
    "    transforms.ToTensor(),\n",
    "    transforms.Normalize([0.485, 0.456, 0.406],\n",
    "                         [0.229, 0.224, 0.225])\n",
    "])\n",
    "\n",
    "val_tf = transforms.Compose([\n",
    "    transforms.ToPILImage(),\n",
    "    transforms.Resize((96, 96)),\n",
    "    transforms.ToTensor(),\n",
    "    transforms.Normalize([0.485, 0.456, 0.406],\n",
    "                         [0.229, 0.224, 0.225])\n",
    "])\n",
    "\n",
    "# 4. 训练 / 验证 划分\n",
    "x_tr, x_val, y_tr, y_val = train_test_split(x_train, y_train,\n",
    "                                            test_size=0.1,\n",
    "                                            random_state=42,\n",
    "                                            stratify=y_train)\n",
    "\n",
    "train_ds = SignMNISTDataset(x_tr, y_tr, transform=train_tf)\n",
    "val_ds   = SignMNISTDataset(x_val, y_val, transform=val_tf)\n",
    "test_ds  = SignMNISTDataset(x_test, y_test, transform=val_tf)\n",
    "\n",
    "train_loader = DataLoader(train_ds, batch_size=64, shuffle=True,  num_workers=2)\n",
    "val_loader   = DataLoader(val_ds,   batch_size=64, shuffle=False, num_workers=2)\n",
    "test_loader  = DataLoader(test_ds,  batch_size=64, shuffle=False, num_workers=2)\n",
    "\n",
    "# 5. 模型：预训练 MobileNetV2 ---------------------------------------------------\n",
    "model = models.mobilenet_v2(pretrained=True)\n",
    "\n",
    "# 冻结特征提取层\n",
    "for param in model.features.parameters():\n",
    "    param.requires_grad = False\n",
    "\n",
    "# 替换分类头：24 类\n",
    "model.classifier[1] = nn.Linear(model.last_channel, 24)\n",
    "model = model.to(device)\n",
    "\n",
    "# 6. 损失 & 优化器\n",
    "criterion = nn.CrossEntropyLoss()\n",
    "optimizer = torch.optim.Adam(model.classifier[1].parameters(), lr=1e-3)\n",
    "\n",
    "# 7. 训练 / 验证函数\n",
    "def run_epoch(loader, training=False):\n",
    "    if training:\n",
    "        model.train()\n",
    "    else:\n",
    "        model.eval()\n",
    "    epoch_loss, correct, total = 0.0, 0, 0\n",
    "    with torch.set_grad_enabled(training):\n",
    "        for x, y in loader:\n",
    "            x, y = x.to(device), y.to(device)\n",
    "            out = model(x)\n",
    "            loss = criterion(out, y)\n",
    "            if training:\n",
    "                optimizer.zero_grad()\n",
    "                loss.backward()\n",
    "                optimizer.step()\n",
    "            epoch_loss += loss.item() * x.size(0)\n",
    "            _, pred = torch.max(out, 1)\n",
    "            correct += (pred == y).sum().item()\n",
    "            total   += y.size(0)\n",
    "    return epoch_loss/total, correct/total\n",
    "\n",
    "# 8. 主循环（10 epoch 示例）\n",
    "best_acc = 0.0\n",
    "for epoch in range(1, 11):\n",
    "    tr_loss, tr_acc = run_epoch(train_loader, training=True)\n",
    "    val_loss, val_acc = run_epoch(val_loader, training=False)\n",
    "    print(f'Epoch {epoch:02d} | '\n",
    "          f'Train loss {tr_loss:.4f} acc {tr_acc:.4f} | '\n",
    "          f'Val loss {val_loss:.4f} acc {val_acc:.4f}')\n",
    "    if val_acc > best_acc:\n",
    "        best_acc = val_acc\n",
    "        torch.save(model.state_dict(), 'best_mobilenetv2_sign.pth')\n",
    "\n",
    "# 9. 测试集评估\n",
    "model.load_state_dict(torch.load('best_mobilenetv2_sign.pth'))\n",
    "test_loss, test_acc = run_epoch(test_loader, training=False)\n",
    "print(f'Test accuracy = {test_acc:.4f}')\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5f49da43-e681-46c2-b361-30563ac697f2",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e97f69b9-4184-40a8-acd9-bf3b157d6a61",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fadd3baa-1a29-4c3d-9424-0cc648fc38c9",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "663a58b3-d20f-4fb0-96c9-94f0eb027825",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f27f1266-758d-4649-aa65-0e3d14e31c49",
   "metadata": {
    "ExecutionIndicator": {
     "show": true
    },
    "tags": []
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3e102e29-1910-4702-aafe-f8c8ae61f68b",
   "metadata": {
    "ExecutionIndicator": {
     "show": true
    },
    "tags": []
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "bad5620f-eeb8-4720-8552-5447abf680b9",
   "metadata": {
    "ExecutionIndicator": {
     "show": true
    },
    "tags": []
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8dbc7021-6cbe-4bc1-8541-b9b2f82de5e9",
   "metadata": {
    "ExecutionIndicator": {
     "show": true
    },
    "tags": []
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e593bfc3-aa69-45bb-84aa-e949bf974c1a",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7395fba3-8060-4fae-a573-0dd8c9527a69",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "596cb84e-2278-4339-8eed-6765d9142576",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "20ad2f09-5016-4d13-828e-e5bab29e352f",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d0acef35-9c51-41a6-b122-0abed897af53",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
